import Cesium from '../Ces/Cesium'
/* eslint-disable no-inner-declarations */
/* eslint-disable no-undef */
/* eslint-disable no-nested-ternary */
/* eslint-disable no-restricted-properties */
/* eslint-disable no-use-before-define */
/* eslint-disable no-useless-concat */
/**
 * Use {@link Viewer#shadowMap} to get the scene's shadow map. Do not construct this directly.
 *
 * <p>
 * The normalOffset bias pushes the shadows forward slightly, and may be disabled
 * for applications that require ultra precise shadows.
 * </p>
 *
 * @alias ShadowMap
 * @internalConstructor
 * @class
 *
 * @param {Object} options An object containing the following properties:
 * @param {Cesium.Camera} options.lightCamera A camera representing the light source.
 * @param {Boolean} [options.enabled=true] Whether the shadow map is enabled.
 * @param {Boolean} [options.isPointLight=false] Whether the light source is a point light. Point light shadows do not use cascades.
 * @param {Number} [options.pointLightRadius=100.0] Radius of the point light.
 * @param {Number} [options.size=2048] The width and height, in pixels, of each shadow map.
 * @param {Boolean} [options.softShadows=false] Whether percentage-closer-filtering is enabled for producing softer shadows.
 * @param {Number} [options.darkness=0.3] The shadow darkness.
 * @param {Boolean} [options.normalOffset=true] Whether a normal bias is applied to shadows.
 * @param {Boolean} [options.fadingEnabled=true] Whether shadows start to fade out once the light gets closer to the horizon.
 *
 * @exception {Cesium.DeveloperError} Only one or four cascades are supported.
 *
 * @demo {@link https://sandcastle.cesium.com/index.html?src=Shadows.html|Cesium Sandcastle Shadows Demo}
 */

let scratchCartesian3;

let waterPlaneMatrix;

let reflectMatrix;

let waterOffsetMatrix;

function ShadowMap(options) {
  options = options ?? Cesium.Frozen.EMPTY_OBJECT
  // options.context is an undocumented option
  const scene = options.scene;

  // >>includeStart('debug', pragmas.debug);
  if (!Cesium.defined(scene)) {
    throw new Cesium.DeveloperError('scene is required.');
  }
  const context = scene.context;

  this._waterHeight = options.waterHeight ?? 0.0
  this._enabled = options.enabled ?? true
  this._softShadows = options.softShadows ?? false
  this._normalOffset = options.normalOffset ?? true
  this.dirty = true;

  /**
   * Specifies whether the shadow map originates from a light source. Shadow maps that are used for analytical
   * purposes should set this to false so as not to affect scene rendering.
   *
   * @private
   */
  this.fromLightSource = options.fromLightSource ?? true

  /**
   * Determines the darkness of the shadows.
   *
   * @type {Number}
   * @default 0.3
   */
  this.darkness = options.darkness ?? 0.3
  this._darkness = this.darkness;

  /**
   * Determines whether shadows start to fade out once the light gets closer to the horizon.
   *
   * @type {Boolean}
   * @default true
   */
  this.fadingEnabled = options.fadingEnabled ?? true

  this._outOfView = false;
  this._outOfViewPrevious = false;
  this._needsUpdate = true;

  // In IE11 and Edge polygon offset is not functional.
  // TODO : Also disabled for instances of Firefox and Chrome running ANGLE that do not support depth textures.
  // Re-enable once https://github.com/CesiumGS/cesium/issues/4560 is resolved.
  let polygonOffsetSupported = true;

  if (
    Cesium.FeatureDetection.isInternetExplorer()
    || Cesium.FeatureDetection.isEdge()
    || ((Cesium.FeatureDetection.isChrome() || Cesium.FeatureDetection.isFirefox())
      && Cesium.FeatureDetection.isWindows()
      && !context.depthTexture)
  ) {
    polygonOffsetSupported = false;
  }
  this._polygonOffsetSupported = polygonOffsetSupported;

  this._terrainBias = {
    polygonOffset: polygonOffsetSupported,
    polygonOffsetFactor: 1.1,
    polygonOffsetUnits: 4.0,
    normalOffset: this._normalOffset,
    normalOffsetScale: 0.5,
    normalShading: true,
    normalShadingSmooth: 0.3,
    depthBias: 0.0001,
  };

  this._primitiveBias = {
    polygonOffset: polygonOffsetSupported,
    polygonOffsetFactor: 1.1,
    polygonOffsetUnits: 4.0,
    normalOffset: this._normalOffset,
    normalOffsetScale: 0.1,
    normalShading: true,
    normalShadingSmooth: 0.05,
    depthBias: 0.00002,
  };

  this._pointBias = {
    polygonOffset: false,
    polygonOffsetFactor: 1.1,
    polygonOffsetUnits: 4.0,
    normalOffset: this._normalOffset,
    normalOffsetScale: 0.0,
    normalShading: true,
    normalShadingSmooth: 0.1,
    depthBias: 0.0005,
  };

  // Cesium.Framebuffer resources
  this._depthAttachment = undefined;
  this._colorAttachment = undefined;

  // Uniforms
  this._clipPlaneMatrix = new Cesium.Matrix4();
  this._reflectMatrix = new Cesium.Matrix4();

  this._reflectTexture = context.defaultTexture;
  this._distance = 0.0;

  this._lightCamera = new Cesium.Camera(scene);
  this._shadowMapCamera = new ShadowMapCamera();
  this._shadowMapCullingVolume = undefined;
  this._boundingSphere = new Cesium.BoundingSphere();

  this._isPointLight = true;
  this._pointLightRadius = options.pointLightRadius ?? 1e+3

  this._textureSize = new Cesium.Cartesian2();

  this._passes = new Array(1);
  this._passes[0] = new ShadowPass(context);

  this.debugShow = options.debugShow ?? false
  this.debugFreezeFrame = false;
  this._debugFreezeFrame = false;
  this._debugCascadeColors = false;
  this._debugLightFrustum = undefined;
  this._debugCameraFrustum = undefined;
  this._debugShadowViewCommand = undefined;

  this._usesDepthTexture = context.depthTexture;

  if (this._isPointLight) {
    this._usesDepthTexture = false;
  }

  // Create render states for shadow casters
  this._primitiveRenderState = undefined;
  this._terrainRenderState = undefined;
  this._pointRenderState = undefined;
  createRenderStates(this);

  // For clearing the shadow map texture every frame
  this._clearCommand = new Cesium.ClearCommand({
    depth: 1.0,
    color: new Cesium.Color(0, 0, 0, 0),
  });

  this._clearPassState = new Cesium.PassState(context);

  this._size = options.size ?? 2048
  this.size = this._size;


  if (!Cesium.defined(scratchCartesian3)) {
    waterOffsetMatrix = new Cesium.Matrix4();
    waterPlaneMatrix = new Cesium.Matrix4();
    reflectMatrix = new Cesium.Matrix4();
    scratchCartesian3 = new Cesium.Cartesian3();
    scaleBiasMatrix = new Cesium.Matrix4(
      0.5,
      0.0,
      0.0,
      0.5,
      0.0,
      0.5,
      0.0,
      0.5,
      0.0,
      0.0,
      0.5,
      0.5,
      0.0,
      0.0,
      0.0,
      1.0,
    );
  }
}

function ShadowPass(context) {
  this.camera = new ShadowMapCamera();
  this.passState = new Cesium.PassState(context);
  this.framebuffer = undefined;
  this.textureOffsets = undefined;
  this.commandList = [];
  this.cullingVolume = undefined;
}

function createRenderState(colorMask, bias) {
  return Cesium.RenderState.fromCache({
    cull: {
      enabled: true,
      face: Cesium.CullFace.BACK,
    },
    depthTest: {
      enabled: true,
    },
    colorMask: {
      red: colorMask,
      green: colorMask,
      blue: colorMask,
      alpha: colorMask,
    },
    depthMask: true,
    polygonOffset: {
      enabled: bias.polygonOffset,
      factor: bias.polygonOffsetFactor,
      units: bias.polygonOffsetUnits,
    },
  });
}

function createRenderStates(shadowMap) {
  // Enable the color mask if the shadow map is backed by a color texture, e.g. when depth textures aren't supported
  const colorMask = !shadowMap._usesDepthTexture;

  shadowMap._primitiveRenderState = createRenderState(
    colorMask,
    shadowMap._primitiveBias,
  );
  shadowMap._terrainRenderState = createRenderState(
    colorMask,
    shadowMap._terrainBias,
  );
  shadowMap._pointRenderState = createRenderState(
    colorMask,
    shadowMap._pointBias,
  );
}

/**
 * @private
 */
ShadowMap.prototype.debugCreateRenderStates = function () {
  createRenderStates(this);
};

Object.defineProperties(ShadowMap.prototype, {

  /**
   * Determines if the shadow map will be shown.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @default true
   */
  enabled: {
    get() {
      return this._enabled;
    },
    set(value) {
      this.dirty = this._enabled !== value;
      this._enabled = value;
    },
  },

  /**
   * Determines if a normal bias will be applied to shadows.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @default true
   */
  normalOffset: {
    get() {
      return this._normalOffset;
    },
    set(value) {
      this.dirty = this._normalOffset !== value;
      this._normalOffset = value;
      this._terrainBias.normalOffset = value;
      this._primitiveBias.normalOffset = value;
      this._pointBias.normalOffset = value;
    },
  },

  /**
   * Determines if soft shadows are enabled. Uses pcf filtering which requires more texture reads and may hurt performance.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @default false
   */
  softShadows: {
    get() {
      return this._softShadows;
    },
    set(value) {
      this.dirty = this._softShadows !== value;
      this._softShadows = value;
    },
  },

  /**
   * The width and height, in pixels, of each shadow map.
   *
   * @memberof ShadowMap.prototype
   * @type {Number}
   * @default 2048
   */
  size: {
    get() {
      return this._size;
    },
    set(value) {
      resize(this, value);
    },
  },

  /**
   * Whether the shadow map is out of view of the scene camera.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @readonly
   * @private
   */
  outOfView: {
    get() {
      return this._outOfView;
    },
  },

  /**
   * The culling volume of the shadow frustum.
   *
   * @memberof ShadowMap.prototype
   * @type {Cesium.CullingVolume}
   * @readonly
   * @private
   */
  shadowMapCullingVolume: {
    get() {
      return this._shadowMapCullingVolume;
    },
  },

  /**
   * The passes used for rendering shadows. Each face of a point light or each cascade for a cascaded shadow map is a separate pass.
   *
   * @memberof ShadowMap.prototype
   * @type {ShadowPass[]}
   * @readonly
   * @private
   */
  passes: {
    get() {
      return this._passes;
    },
  },

  /**
   * Whether the light source is a point light.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @readonly
   * @private
   */
  isPointLight: {
    get() {
      return this._isPointLight;
    },
  },

  /**
   * Debug option for visualizing the cascades by color.
   *
   * @memberof ShadowMap.prototype
   * @type {Boolean}
   * @default false
   * @private
   */
  debugCascadeColors: {
    get() {
      return this._debugCascadeColors;
    },
    set(value) {
      this.dirty = this._debugCascadeColors !== value;
      this._debugCascadeColors = value;
    },
  },
});

function destroyFramebuffer(shadowMap) {
  const length = shadowMap._passes.length;

  for (let i = 0; i < length; ++i) {
    const pass = shadowMap._passes[i];
    const framebuffer = pass.framebuffer;

    if (Cesium.defined(framebuffer) && !framebuffer.isDestroyed()) {
      framebuffer.destroy();
    }
    pass.framebuffer = undefined;
  }

  // Destroy the framebuffer attachments
  shadowMap._depthAttachment = shadowMap._depthAttachment && shadowMap._depthAttachment.destroy();
  shadowMap._colorAttachment = shadowMap._colorAttachment && shadowMap._colorAttachment.destroy();
}

function createFramebufferColor(shadowMap, context) {
  const depthRenderbuffer = new Cesium.Renderbuffer({
    context,
    width: shadowMap._textureSize.x,
    height: shadowMap._textureSize.y,
    format: Cesium.RenderbufferFormat.DEPTH_COMPONENT16,
  });

  const colorTexture = new Cesium.Texture({
    context,
    width: shadowMap._textureSize.x,
    height: shadowMap._textureSize.y,
    pixelFormat: Cesium.PixelFormat.RGBA,
    pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
    sampler: Cesium.Sampler.NEAREST,
  });

  const framebuffer = new Cesium.Framebuffer({
    context,
    depthRenderbuffer,
    colorTextures: [colorTexture],
    destroyAttachments: false,
  });

  const length = shadowMap._passes.length;

  for (let i = 0; i < length; ++i) {
    const pass = shadowMap._passes[i];

    pass.framebuffer = framebuffer;
    pass.passState.framebuffer = framebuffer;
  }

  shadowMap._reflectTexture = colorTexture;
  shadowMap._depthAttachment = depthRenderbuffer;
  shadowMap._colorAttachment = colorTexture;
}


function createFramebuffer(shadowMap, context) {
  createFramebufferColor(shadowMap, context);
}

function checkFramebuffer(shadowMap, context) {
  // Attempt to make an FBO with only a depth texture. If it fails, fallback to a color texture.
  if (
    shadowMap._usesDepthTexture
    && shadowMap._passes[0].framebuffer.status
    !== Cesium.WebGLConstants.FRAMEBUFFER_COMPLETE
  ) {
    shadowMap._usesDepthTexture = false;
    createRenderStates(shadowMap);
    destroyFramebuffer(shadowMap);
    createFramebuffer(shadowMap, context);
  }
}

function updateFramebuffer(shadowMap, context) {
  if (
    !Cesium.defined(shadowMap._passes[0].framebuffer)
    || shadowMap._reflectTexture.width !== shadowMap._textureSize.x
  ) {
    destroyFramebuffer(shadowMap);
    createFramebuffer(shadowMap, context);
    checkFramebuffer(shadowMap, context);
    clearFramebuffer(shadowMap, context);
  }
}

function clearFramebuffer(shadowMap, context, shadowPass) {
  shadowPass = shadowPass ?? 0
  shadowMap._clearCommand.framebuffer = shadowMap._passes[shadowPass].framebuffer;
  shadowMap._clearCommand.execute(context, shadowMap._clearPassState);
}

function resize(shadowMap, size) {
  shadowMap._size = size;
  const passes = shadowMap._passes;
  const numberOfPasses = passes.length;
  const textureSize = shadowMap._textureSize;

  size = Cesium.ContextLimits.maximumTextureSize >= size
    ? size
    : Cesium.ContextLimits.maximumTextureSize;
  textureSize.x = size;
  textureSize.y = size;
  passes[0].passState.viewport = new Cesium.BoundingRectangle(0, 0, textureSize.x, textureSize.y);

  // Update clear pass state
  shadowMap._clearPassState.viewport = new Cesium.BoundingRectangle(
    0,
    0,
    textureSize.x,
    textureSize.y,
  );

  // Transforms shadow coordinates [0, 1] into the pass's region of the texture
  for (let i = 0; i < numberOfPasses; ++i) {
    const pass = passes[i];
    const viewport = pass.passState.viewport;
    const biasX = viewport.x / textureSize.x;
    const biasY = viewport.y / textureSize.y;
    const scaleX = viewport.width / textureSize.x;
    const scaleY = viewport.height / textureSize.y;

    pass.textureOffsets = new Cesium.Matrix4(
      scaleX,
      0.0,
      0.0,
      biasX,
      0.0,
      scaleY,
      0.0,
      biasY,
      0.0,
      0.0,
      1.0,
      0.0,
      0.0,
      0.0,
      0.0,
      1.0,
    );
  }
}

let scratchViewport;

function createDebugShadowViewCommand(shadowMap, context) {
  const fs = `${'uniform sampler2D shadowMap_texture; \n'
    + 'in vec2 v_textureCoordinates; \n'
    + 'void main() \n'
    + '{ \n'
    } out_FragColor = texture(shadowMap_texture, v_textureCoordinates); \n` + '} \n';

  const drawCommand = context.createViewportQuadCommand(fs, {
    uniformMap: {
      shadowMap_texture() {
        return shadowMap._reflectTexture;
      },
    },
  });

  drawCommand.pass = Cesium.Pass.OVERLAY;

  return drawCommand;
}

function updateDebugShadowViewCommand(shadowMap, frameState) {
  // Draws the shadow map on the bottom-right corner of the screen
  const context = frameState.context;
  const screenWidth = frameState.context.drawingBufferWidth;
  const screenHeight = frameState.context.drawingBufferHeight;
  const size = Math.min(screenWidth, screenHeight) * 0.3;

  if (!Cesium.defined(scratchViewport)) {
    scratchViewport = new Cesium.BoundingRectangle();
  }
  const viewport = scratchViewport;

  viewport.x = screenWidth - size;
  viewport.y = 0;
  viewport.width = size;
  viewport.height = size;

  let debugCommand = shadowMap._debugShadowViewCommand;

  if (!Cesium.defined(debugCommand)) {
    debugCommand = createDebugShadowViewCommand(shadowMap, context);
    shadowMap._debugShadowViewCommand = debugCommand;
  }

  // Get a new Cesium.RenderState for the updated viewport size
  if (
    !Cesium.defined(debugCommand.renderState)
    || !Cesium.BoundingRectangle.equals(debugCommand.renderState.viewport, viewport)
  ) {
    debugCommand.renderState = Cesium.RenderState.fromCache({
      viewport: Cesium.BoundingRectangle.clone(viewport),
    });
  }

  frameState.commandList.push(shadowMap._debugShadowViewCommand);
}

let scratchMatrix;

function createDebugPointLight(modelMatrix, color) {
  const box = new Cesium.GeometryInstance({
    geometry: new Cesium.BoxOutlineGeometry({
      minimum: new Cesium.Cartesian3(-0.5, -0.5, -0.5),
      maximum: new Cesium.Cartesian3(0.5, 0.5, 0.5),
    }),
    attributes: {
      color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),
    },
  });

  const sphere = new Cesium.GeometryInstance({
    geometry: new Cesium.SphereOutlineGeometry({
      radius: 0.5,
    }),
    attributes: {
      color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),
    },
  });

  return new Cesium.Primitive({
    geometryInstances: [box, sphere],
    appearance: new Cesium.PerInstanceColorAppearance({
      translucent: false,
      flat: true,
    }),
    asynchronous: false,
    modelMatrix,
  });
}

let scratchScale;

function applyDebugSettings(shadowMap, frameState) {
  if (!Cesium.defined(scratchMatrix)) {
    scratchMatrix = new Cesium.Matrix4();
    scratchScale = new Cesium.Cartesian3();
  }
  updateDebugShadowViewCommand(shadowMap, frameState);

  shadowMap._debugFreezeFrame = shadowMap.debugFreezeFrame;

  if (!Cesium.defined(shadowMap._debugLightFrustum) || shadowMap._needsUpdate) {
    const translation = shadowMap._shadowMapCamera.positionWC;
    const rotation = Cesium.Quaternion.IDENTITY;
    const uniformScale = shadowMap._pointLightRadius * 2.0;
    const scale = Cesium.Cartesian3.fromElements(
      uniformScale,
      uniformScale,
      uniformScale,
      scratchScale,
    );
    const modelMatrix = Cesium.Matrix4.fromTranslationQuaternionRotationScale(
      translation,
      rotation,
      scale,
      scratchMatrix,
    );

    shadowMap._debugLightFrustum = shadowMap._debugLightFrustum && shadowMap._debugLightFrustum.destroy();
    shadowMap._debugLightFrustum = createDebugPointLight(
      modelMatrix,
      Cesium.Color.YELLOW,
    );
  }
  shadowMap._debugLightFrustum.update(frameState);
}

function ShadowMapCamera() {
  this.viewMatrix = new Cesium.Matrix4();
  this.inverseViewMatrix = new Cesium.Matrix4();
  this.frustum = undefined;
  this.positionCartographic = new Cesium.Cartographic();
  this.positionWC = new Cesium.Cartesian3();
  this.positionEC = new Cesium.Cartesian3();
  this.directionWC = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Z);
  this.upWC = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Y);
  this.rightWC = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_X);
  this.viewProjectionMatrix = new Cesium.Matrix4();
}

ShadowMapCamera.prototype.clone = function (camera) {
  Cesium.Matrix4.clone(camera.viewMatrix, this.viewMatrix);
  Cesium.Matrix4.clone(camera.inverseViewMatrix, this.inverseViewMatrix);
  this.frustum = camera.frustum.clone(this.frustum);
  Cesium.Cartographic.clone(camera.positionCartographic, this.positionCartographic);
  Cesium.Cartesian3.clone(camera.positionWC, this.positionWC);
  Cesium.Cartesian3.clone(camera.directionWC, this.directionWC);
  Cesium.Cartesian3.clone(camera.upWC, this.upWC);
  Cesium.Cartesian3.clone(camera.rightWC, this.rightWC);
};

// Converts from NDC space to texture space
let scaleBiasMatrix;

ShadowMapCamera.prototype.getViewProjection = function () {
  if (!Cesium.defined(scaleBiasMatrix)) {
    scaleBiasMatrix = new Cesium.Matrix4(
      0.5,
      0.0,
      0.0,
      0.5,
      0.0,
      0.5,
      0.0,
      0.5,
      0.0,
      0.0,
      0.5,
      0.5,
      0.0,
      0.0,
      0.0,
      1.0,
    );
  }
  const view = this.viewMatrix;
  const projection = this.frustum.projectionMatrix;

  Cesium.Matrix4.multiply(projection, view, this.viewProjectionMatrix);
  Cesium.Matrix4.multiply(
    scaleBiasMatrix,
    this.viewProjectionMatrix,
    this.viewProjectionMatrix,
  );

  return this.viewProjectionMatrix;
};

let scratchBoundingSphere;

let scratchCenter;

function checkVisibility(shadowMap, frameState) {
  if (!Cesium.defined(scratchBoundingSphere)) {
    scratchBoundingSphere = new Cesium.BoundingSphere();
    scratchCenter = scratchBoundingSphere.center;
  }

  const shadowMapCamera = shadowMap._shadowMapCamera;

  const boundingSphere = scratchBoundingSphere;

  boundingSphere.center = shadowMapCamera.positionWC;
  boundingSphere.radius = shadowMap._pointLightRadius;
  shadowMap._outOfView = frameState.cullingVolume.computeVisibility(boundingSphere)
    === Cesium.Intersect.OUTSIDE;
  shadowMap._needsUpdate = !shadowMap._outOfView
    && !shadowMap._boundingSphere.equals(boundingSphere);
  Cesium.BoundingSphere.clone(boundingSphere, shadowMap._boundingSphere);
}

function updateCameras(shadowMap, frameState) {
  const camera = frameState.camera; // The actual camera in the scene
  const shadowMapCamera = shadowMap._shadowMapCamera; // Cesium.Camera representing the shadow volume, initially cloned from lightCamera

  Cesium.Cartesian3.clone(camera.positionWC, shadowMapCamera.positionWC);

  shadowMap._shadowMapCamera = Cesium.Camera.clone(camera, shadowMapCamera);

  checkVisibility(shadowMap, frameState);

  if (!shadowMap._outOfViewPrevious && shadowMap._outOfView) {
    shadowMap._needsUpdate = true;
  }
  shadowMap._outOfViewPrevious = shadowMap._outOfView;
}

/**
 * @private
 */
ShadowMap.prototype.update = function (frameState) {
  updateCameras(this, frameState);

  if (this._needsUpdate) {
    updateFramebuffer(this, frameState.context);

    const sceneCamera = frameState.camera;
    const reflectCamera = this._lightCamera;

    const heightAboveWater = sceneCamera.positionCartographic.height - this._waterHeight;

    const cameraPosition = Cesium.Matrix4.multiplyByPoint(sceneCamera._transform, sceneCamera.position, new Cesium.Cartesian3());

    Cesium.Transforms.eastNorthUpToFixedFrame(cameraPosition, Cesium.Ellipsoid.WGS84, waterPlaneMatrix);

    const offsetMatrix = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(0, 0, -heightAboveWater), waterOffsetMatrix);

    Cesium.Matrix4.multiply(
      waterPlaneMatrix,
      offsetMatrix,
      waterPlaneMatrix,
    );

    const reflectCameraPlaneMatrix = Cesium.Matrix4.multiply(
      waterPlaneMatrix,
      offsetMatrix,
      reflectMatrix,
    );

    const destination = Cesium.Matrix4.getTranslation(
      reflectCameraPlaneMatrix,
      reflectCamera.position,
    );

    reflectCamera.setView({
      destination,
      orientation: {
        heading: sceneCamera.heading,
        pitch: -sceneCamera.pitch,
        roll: sceneCamera.roll,
      },
    });

    this._passes[0].camera.clone(reflectCamera);

    const reflectProjectViewMatrix = this._reflectMatrix;

    Cesium.Matrix4.multiply(
      reflectCamera.frustum.projectionMatrix,
      reflectCamera.viewMatrix,
      reflectProjectViewMatrix,
    );
    Cesium.Matrix4.multiply(
      scaleBiasMatrix,
      reflectProjectViewMatrix,
      reflectProjectViewMatrix,
    );
    Cesium.Matrix4.multiply(
      reflectProjectViewMatrix,
      reflectCamera.inverseViewMatrix,
      reflectProjectViewMatrix,
    );

    this._shadowMapCullingVolume = Cesium.CullingVolume.fromBoundingSphere(
      this._boundingSphere,
    );

    let clippingPlanesMatrix = waterPlaneMatrix;

    clippingPlanesMatrix = Cesium.Matrix4.multiply(
      reflectCamera.viewMatrix,
      waterPlaneMatrix,
      clippingPlanesMatrix,
    );

    this._clipPlaneMatrix = Cesium.Matrix4.inverseTranspose(
      clippingPlanesMatrix,
      this._clipPlaneMatrix,
    );
  }

  if (this.debugShow) {
    applyDebugSettings(this, frameState);
  }
};

/**
 * @private
 */
ShadowMap.prototype.updatePass = function (context, shadowPass) {
  clearFramebuffer(this, context, shadowPass);
};

function combineUniforms(shadowMap, uniforms, isTerrain) {
  const mapUniforms = {
    shadowMap_texture() {
      return shadowMap._reflectTexture;
    },
    clipPlaneMatrix() {
      return shadowMap._clipPlaneMatrix;
    },
  };

  return Cesium.combine(uniforms, mapUniforms, false);
}

function createCastDerivedCommand(
  shadowMap,
  shadowsDirty,
  command,
  context,
  oldShaderId,
  result,
) {
  let castShader;

  let castRenderState;

  let castUniformMap;

  if (Cesium.defined(result)) {
    castShader = result.shaderProgram;
    castRenderState = result.renderState;
    castUniformMap = result.uniformMap;
  }

  result = Cesium.DrawCommand.shallowClone(command, result);
  result.castShadows = true;
  result.receiveShadows = false;

  if (
    !Cesium.defined(castShader)
    || oldShaderId !== command.shaderProgram.id
    || shadowsDirty
  ) {
    const shaderProgram = command.shaderProgram;

    const isTerrain = command.pass === Cesium.Pass.GLOBE;
    const isOpaque = command.pass !== Cesium.Pass.TRANSLUCENT;
    const isPointLight = shadowMap._isPointLight;
    const usesDepthTexture = shadowMap._usesDepthTexture;

    const keyword = Cesium.ShadowMapShader.getShadowCastShaderKeyword(
      isPointLight,
      isTerrain,
      usesDepthTexture,
      isOpaque,
    );

    castShader = context.shaderCache.getDerivedShaderProgram(
      shaderProgram,
      keyword,
    );
    if (!Cesium.defined(castShader)) {
      const vertexShaderSource = shaderProgram.vertexShaderSource;
      const fragmentShaderSource = shaderProgram.fragmentShaderSource;

      const castVS = Cesium.ShadowMapShader.createShadowCastVertexShader(
        vertexShaderSource,
        isPointLight,
        isTerrain,
      );
      const castFS = Cesium.ShadowMapShader.createShadowCastFragmentShader(
        fragmentShaderSource,
        isPointLight,
        usesDepthTexture,
        isOpaque,
      );

      castShader = context.shaderCache.createDerivedShaderProgram(
        shaderProgram,
        keyword,
        {
          vertexShaderSource: castVS,
          fragmentShaderSource: castFS,
          attributeLocations: shaderProgram._attributeLocations,
        },
      );
    }

    castRenderState = shadowMap._primitiveRenderState;
    if (isPointLight) {
      castRenderState = shadowMap._pointRenderState;
    } else if (isTerrain) {
      castRenderState = shadowMap._terrainRenderState;
    }

    // Modify the render state for commands that do not use back-face culling, e.g. flat textured walls
    const cullEnabled = command.renderState.cull.enabled;

    if (!cullEnabled) {
      castRenderState = Cesium.clone(castRenderState, false);
      castRenderState.cull = Cesium.clone(castRenderState.cull, false);
      castRenderState.cull.enabled = false;
      castRenderState = Cesium.RenderState.fromCache(castRenderState);
    }

    castUniformMap = combineUniforms(shadowMap, command.uniformMap, isTerrain);
  }

  result.shaderProgram = castShader;
  result.renderState = castRenderState;
  result.uniformMap = castUniformMap;

  return result;
}

ShadowMap.createReceiveDerivedCommand = function (
  lightShadowMaps,
  command,
  shadowsDirty,
  context,
  result,
) {
  if (!Cesium.defined(result)) {
    result = {};
  }

  const lightShadowMapsEnabled = lightShadowMaps.length > 0;

  if (command.receiveShadows && lightShadowMapsEnabled) {
    result.receiveCommand = command;
    result.castShadows = false;
    result.receiveShadows = false;
    result.receiveShaderCastShadows = false;
  }

  return result;
};

ShadowMap.createCastDerivedCommand = function (
  shadowMaps,
  command,
  shadowsDirty,
  context,
  result,
) {
  if (!Cesium.defined(result)) {
    result = {};
  }

  if (command.castShadows) {
    let castCommands = result.castCommands;

    if (!Cesium.defined(castCommands)) {
      castCommands = result.castCommands = [];
    }

    const oldShaderId = result.castShaderProgramId;

    const shadowMapLength = shadowMaps.length;

    castCommands.length = shadowMapLength;

    for (let i = 0; i < shadowMapLength; ++i) {
      castCommands[i] = createCastDerivedCommand(
        shadowMaps[i],
        shadowsDirty,
        command,
        context,
        oldShaderId,
        castCommands[i],
      );
    }

    result.castShaderProgramId = command.shaderProgram.id;
  }

  return result;
};

/**
 * @private
 */
ShadowMap.prototype.isDestroyed = function () {
  return false;
};

/**
 * @private
 */
ShadowMap.prototype.destroy = function () {
  destroyFramebuffer(this);

  this._debugLightFrustum = this._debugLightFrustum && this._debugLightFrustum.destroy();
  this._debugCameraFrustum = this._debugCameraFrustum && this._debugCameraFrustum.destroy();
  this._debugShadowViewCommand = this._debugShadowViewCommand
    && this._debugShadowViewCommand.shaderProgram
    && this._debugShadowViewCommand.shaderProgram.destroy();

  return Cesium.destroyObject(this);
};
export default ShadowMap;
